from logging import *
from argparse import ArgumentParser
import os
#import fsnotify
import ffmpeg
import sniff
import common as common
import copy
from dataclasses import *
__version__ = "v0.1.0"

logger = Logger(__name__)

def main():
    parser = ArgumentParser(
        description="Unlock your encrypted music file")
    parser.add_argument(
        '--input', '-i', help='path to input file or dir', required=False)
    parser.add_argument(
        '--output', '-o', help='path to output dir', required=False)
    parser.add_argument(
        '--qmc-mmkv', '-db', help='path to qmc mmkv (.crc file also required)', required=False)
    parser.add_argument('--qmc-mmkv-key', '-key',
                        help='mmkv password (16 ascii chars)', required=False)
    parser.add_argument('--remove-source', '-rs', action='store_true',
                        help='remove source file', required=False)
    parser.add_argument('--skip-noop', '-n', action='store_true',
                        help='skip noop decoder', required=False)
    parser.add_argument('--verbose', '-V', action='store_true',
                        help='verbose logging', required=False)
    parser.add_argument('--update-metadata', action='store_true',
                        help='update metadata & album art from network', required=False)
    parser.add_argument('--overwrite', action='store_true',
                        help='overwrite output file without asking', required=False)
    parser.add_argument('--watch', action='store_true',
                        help='watch the input dir and process new files', required=False)
    parser.add_argument('--supported-ext', action='store_true',
                        help='show supported file extensions and exit', required=False)
    args = parser.parse_args()
    app_main(args)

def print_supported_ext():
    exts = []
    extset = {}
    for factory in common.get_registery():
        ext = factory.suffix.lstrip(".")
        if ext not in extset:
            extset[ext] = 1
        else:
            extset[ext] += 1
    for ext in extset:
        exts.append(ext)
    exts.sort()
    print("Supported file extensions:")
    for ext in exts:
        print("%s: %d"%(ext, extset[ext]))
    
def setupLogger(verbose):
    return Logger(__name__, level=DEBUG if verbose else INFO)

def app_main(*args):
    logger = setupLogger(args.verbose)
    cwd = os.getcwd()
    if args.supported_ext:
        print_supported_ext()
        return
    input = args.input
    if not input:
        if len(args)==0:
            input = cwd
        elif len(args)==1:
            input = args[0]
        else:
            raise ValueError("please specify input file (or directory)")
    
    input = os.path.abspath(input)
    
    output = args.output
    inputstat = os.stat(input)
    if os.path.isdir(input):
        inputdir = input
    else:
        inputdir = os.path.dirname(input)
    inputdir = os.path.abspath(inputdir)
    if not output:
        output = inputdir
    logger.debug("input: %s, output: %s", input, output)
    try:
        outputstat = os.stat(output)
    except FileNotFoundError:
        os.makedirs(output, 0o755)
        outputstat = os.stat(output)
    if not os.path.isdir(output):
        raise ValueError("output must be a directory")
    if mmkv := args.qmc_mmkv:
        key = args.qmc_mmkv_key
        qmc.openMMKV(mmkv, key, logger)
    proc = Processor(
        logger = logger,
        input_dir = inputdir,
        output_dir = output,
        skip_noop_decoder = args.skip_noop,
        remove_source = args.remove_source,
        update_metadata = args.update_metadata,
        overwrite_output = args.overwrite
    )
    if os.path.isdir(input):
        watchdir = args.watch
        if watchdir:
            proc.watchdir(input)
        else:
            proc.processdir(input)
    else:
        proc.processfile(input)

@dataclass
class Processor:
    logger: Logger
    input_dir: str
    output_dir: str
    skip_noop_decoder: bool
    remove_source: bool
    update_metadata: bool
    overwrite_output: bool
    def watchdir(self,inputDir):
        raise NotImplementedError()
    def processdir(self,inputDir):
        items = os.listdir(inputDir)
        for item in items:
            filepath = os.path.join(inputDir, item)
            if os.path.isdir(filepath):
                try:
                    self.processdir(filepath)
                except Exception as e:
                    self.logger.error(f"Error processing directory {filepath}: {e}")
            else:
                try:
                    self.processfile(filepath)
                except Exception as e:
                    self.logger.error(f"Error processing file {filepath}: {e}")
    def processfile(self,filepath):
        self.logger.debug(f"Processing file {filepath}")
        alldec = common.get_decoders(filepath, self.skip_noop_decoder)
        if len(alldec) == 0:
            raise ValueError("No decoder found for file %s"%filepath)
        self.process(filepath, alldec)
        if self.remove_source:
            os.remove(filepath)
            self.logger.info(f"Removed source file {filepath}")
        
    def find_decoder(self, decoders:list[common.DecoderFactory], params:common.DecoderParams):
        for factory in decoders:
            dec = factory.create(params)
            try:
                dec.validate()
            except Exception as e:
                self.logger.warn(f"Decoder {factory.name} failed validation: {e}")
                continue        
            else:
                return dec,factory
        raise ValueError("No decoder found for file")
    def process(self, inputfile, alldec:list[common.DecoderFactory]):
        file = open(inputfile,"rb")
        
        logger = Logger("Processor: "+inputfile)
        logger.parent = self.logger
        pdec, decoderfactory = self.find_decoder(alldec, common.DecoderParams(
            reader = file,
            extension = os.path.splitext(inputfile)[1],
            filepath = inputfile,
            logger = logger                                                                      
        ))
        dec = copy.deepcopy(pdec)
        params = ffmpeg.FfmpegUpdateMetadataParams()
        
        file.seek(0)
        header = file.read(64)
        file.seek(0)
        audio = file
        params.audio_ext = sniff.audio_extension_with_fallback(header, dec.extension)
        if self.update_metadata:
            pass
            # TODO
        inputreldir = os.path.relpath(os.path.dirname(inputfile), self.inputdir)
        infilename_noext = os.path.splitext(os.path.basename(inputfile))[0]
        outpath = os.path.join(self.outputdir, inputreldir, infilename_noext+"."+params.audio_ext)
        if not self.overwrite_output:
            if os.path.exists(outpath):
                raise ValueError(f"Output file {outpath} already exists")
        if not params.meta:
            outfile = open(outpath, "wb+")
            outfile.write(audio.read())
            outfile.close()
        else:
            ffmpeg.update_metadata(audio, params, outpath, logger)
        
        logger.info(f"Output file {outpath} created")
        
        file.close()
                
            
if __name__ == "__main__":
    main()